// NTheora.h

#pragma once

using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::Drawing;
using namespace System::Drawing::Imaging;
using namespace System::Threading::Tasks;

#include<theora\theoraenc.h>
#include<theora\theoradec.h>

namespace NTheora 
{
	// assuming I420 format
	public ref class YCbCrImage
	{
	public:

		YCbCrImage(int width, int height)
		{
			Width = width;
			Height = height;
			int pixels = Width * Height;

			Y = new th_img_plane();
			Y->width = Width;
			Y->height = Height;
			Y->stride = Width;
			Y->data = new unsigned char[pixels];
			
			int cbcrWidth = width / 2;
			int cbcrHeight = height / 2;
			int cbcrPixels = cbcrWidth * cbcrHeight;
			Cb = new th_img_plane();
			Cb->width = cbcrWidth;
			Cb->height = cbcrHeight;
			Cb->stride = cbcrWidth;
			Cb->data = new unsigned char[cbcrPixels];
			
			Cr = new th_img_plane();
			Cr->width = cbcrWidth;
			Cr->height = cbcrHeight;
			Cr->stride = cbcrWidth;
			Cr->data = new unsigned char[cbcrPixels];
		};

		static YCbCrImage^ CreateFromRGB24(IntPtr frame, int width, int height)
		{
			YCbCrImage^ result = gcnew YCbCrImage(width, height);
			int pixels = width * height;
			int stride = width * 3;
			unsigned char *frameData = (unsigned char*)(frame.ToPointer());

			// todo: flip horizontal; row0 -> rowHeight

			int ci = 0;
			for (int y = 0; y < height; y+=2)
				for(int x = 0; x < width; x+=2)
	            {
					int xi = 3 * x;
					int yi = height - y - 1;
	                unsigned char r00 = frameData[yi*stride + xi];
	                unsigned char g00 = frameData[yi*stride + xi+1];
	                unsigned char b00 = frameData[yi*stride + xi+2];

					unsigned char r01 = frameData[yi*stride + xi+3];
	                unsigned char g01 = frameData[yi*stride + xi+4];
	                unsigned char b01 = frameData[yi*stride + xi+5];

					unsigned char r10 = frameData[(yi-1)*stride + xi];
	                unsigned char g10 = frameData[(yi-1)*stride + xi+1];
	                unsigned char b10 = frameData[(yi-1)*stride + xi+2];

					unsigned char r11 = frameData[(yi-1)*stride + xi+3];
	                unsigned char g11 = frameData[(yi-1)*stride + xi+4];
	                unsigned char b11 = frameData[(yi-1)*stride + xi+5];

	                result->Y->data[y*width + x]  = (( 66 * r00 + 129 * g00 +  25 * b00 + 128) >> 8) +  16;
					result->Y->data[y*width + x+1]  = (( 66 * r01 + 129 * g01 +  25 * b01 + 128) >> 8) +  16;
					result->Y->data[(y+1)*width + x]  = (( 66 * r10 + 129 * g10 +  25 * b10 + 128) >> 8) +  16;
					result->Y->data[(y+1)*width + x+1]  = (( 66 * r11 + 129 * g11 +  25 * b11 + 128) >> 8) +  16;

	                unsigned char cb00 = ((112 * r00 -  94 * g00 -  18 * b00 + 128) >> 8) + 128;
					unsigned char cb01 = ((112 * r01 -  94 * g01 -  18 * b01 + 128) >> 8) + 128;
					unsigned char cb10 = ((112 * r10 -  94 * g10 -  18 * b10 + 128) >> 8) + 128;
					unsigned char cb11 = ((112 * r11 -  94 * g11 -  18 * b11 + 128) >> 8) + 128;

					unsigned char cr00 = ((-38 * r00 -  74 * g00 + 112 * b00 + 128) >> 8) + 128;
	                unsigned char cr01 = ((-38 * r01 -  74 * g01 + 112 * b01 + 128) >> 8) + 128;
					unsigned char cr10 = ((-38 * r10 -  74 * g10 + 112 * b10 + 128) >> 8) + 128;
					unsigned char cr11 = ((-38 * r11 -  74 * g11 + 112 * b11 + 128) >> 8) + 128;

					unsigned char cbAverage = (cb00 + cb01 + cb10 + cb11) / 4;
					unsigned char crAverage = (cr00 + cr01 + cr10 + cr11) / 4;

					result->Cb->data[ci] = cbAverage;
					result->Cr->data[ci++] = crAverage;
	            }
			
			return result;
		}

		static YCbCrImage^ CreateFromI420(IntPtr frame, int width, int height)
		{
			return gcnew YCbCrImage(frame, width, height);
		}

		!YCbCrImage()
		{
			delete[] Y->data;
			delete[] Cb->data;
			delete[] Cr->data;

			delete Y;
			delete Cb;
			delete Cr;
		};
		
		~YCbCrImage()
		{
			this->!YCbCrImage();
		};

		void ToRGB(array<Byte>^ rgbData)
		{
			int i = 0;
			for (int yCord = 0; yCord < Height; yCord++)
			{
				for (int xCord = 0; xCord < Width; xCord+=2)
				{
					int c1 = Y->data[yCord * Y->stride + xCord] - 16;
					int c2 = Y->data[yCord * Y->stride + xCord + 1] - 16;
	                int d = Cb->data[yCord / 2 * Cb->stride + xCord / 2] - 128;
	                int e = Cr->data[yCord / 2 * Cr->stride + xCord / 2] - 128;
					
					rgbData[i++] = Math::Min(255, Math::Max(0, (298 * c1           + 409 * e + 128) >> 8));//r
					rgbData[i++] = Math::Min(255, Math::Max(0, (298 * c1 - 100 * d - 208 * e + 128) >> 8));//g
					rgbData[i++] = Math::Min(255, Math::Max(0, (298 * c1 + 516 * d           + 128) >> 8));//b
					
					rgbData[i++] = Math::Min(255, Math::Max(0, (298 * c2           + 409 * e + 128) >> 8));//r
					rgbData[i++] = Math::Min(255, Math::Max(0, (298 * c2 - 100 * d - 208 * e + 128) >> 8));//g
					rgbData[i++] = Math::Min(255, Math::Max(0, (298 * c2 + 516 * d           + 128) >> 8));//b
				}
			}
		};

		int Width;
		int Height;

	internal:
		th_img_plane* Y;
		th_img_plane* Cb;
		th_img_plane* Cr;

	private:
		YCbCrImage(IntPtr frame, int width, int height)
		{
			Width = width;
			Height = height;

			int pixels = width * height;
			Y = new th_img_plane();
			Y->width = width;
			Y->height = height;
			Y->stride = width;
			Y->data = new unsigned char[pixels];
			
			int cbcrWidth = width / 2;
			int cbcrHeight = height / 2;
			int cbcrPixels = cbcrWidth * cbcrHeight;
			Cb = new th_img_plane();
			Cb->width = cbcrWidth;
			Cb->height = cbcrHeight;
			Cb->stride = cbcrWidth;
			Cb->data = new unsigned char[cbcrPixels];
			
			Cr = new th_img_plane();
			Cr->width = cbcrWidth;
			Cr->height = cbcrHeight;
			Cr->stride = cbcrWidth;
			Cr->data = new unsigned char[cbcrPixels];

			unsigned char *frameData = (unsigned char*)(frame.ToPointer());
			unsigned int offset = 0;
			// copy y plane
			memcpy(Y->data, frameData, pixels);
			offset += pixels;
			
			// copy u plane
			int cBytes = width / 2 * height / 2;
			memcpy(Cb->data, frameData + offset, cBytes);
			offset += cBytes;

			// copy v plane
			memcpy(Cr->data, frameData + offset, cBytes);
		}

		// todo: use I420 format
		YCbCrImage(Bitmap^ rgbImage)
		{
			rgbImage->RotateFlip(RotateFlipType::RotateNoneFlipY);

			if (rgbImage->PixelFormat != PixelFormat::Format24bppRgb)
                throw gcnew InvalidOperationException();

			Width = rgbImage->Width;
			Height = rgbImage->Height;

			int pixels = Width * Height;
			Y = new th_img_plane();
			Y->width = Width;
			Y->height = Height;
			Y->stride = Width;
			Y->data = new unsigned char[pixels];
			
			Cb = new th_img_plane();
			Cb->width = Width;
			Cb->height = Height;
			Cb->stride = Width;
			Cb->data = new unsigned char[pixels];
			
			Cr = new th_img_plane();
			Cr->width = Width;
			Cr->height = Height;
			Cr->stride = Width;
			Cr->data = new unsigned char[pixels];

            // coefficients taken from the color space faq
            /* http://msdn.microsoft.com/en-us/library/aa917087.aspx
			  
			  RGB -> YUV
			  Y = ( (  66 * R + 129 * G +  25 * B + 128) >> 8) +  16
			  U = ( ( -38 * R -  74 * G + 112 * B + 128) >> 8) + 128
			  V = ( ( 112 * R -  94 * G -  18 * B + 128) >> 8) + 128



			  YUV -> RGB
			  C = Y - 16
			  D = U - 128
			  E = V - 128
			  
			  R = clip(( 298 * C           + 409 * E + 128) >> 8)
			  G = clip(( 298 * C - 100 * D - 208 * E + 128) >> 8)
			  B = clip(( 298 * C + 516 * D           + 128) >> 8)
            */

			BitmapData^ data = rgbImage->LockBits(*(new Rectangle(Point::Empty, rgbImage->Size)), ImageLockMode::ReadOnly, rgbImage->PixelFormat);
			for(int i = 0; i < pixels; ++i)
            {
                unsigned char r = ((unsigned char*)(data->Scan0.ToPointer()))[i * 3];
                unsigned char g = ((unsigned char*)(data->Scan0.ToPointer()))[i * 3 + 1];
                unsigned char b = ((unsigned char*)(data->Scan0.ToPointer()))[i * 3 + 2];

                Y->data[i]  = (( 66 * r + 129 * g +  25 * b + 128) >> 8) +  16;
                Cb->data[i] = ((112 * r -  94 * g -  18 * b + 128) >> 8) + 128;
                Cr->data[i] = ((-38 * r -  74 * g + 112 * b + 128) >> 8) + 128;
            }
			rgbImage->UnlockBits(data);
		};
	};

	public ref class TheoraPacket
	{
	private:
		ogg_packet* packet;

	public:

		TheoraPacket(array<Byte>^ data)
		{
			this->packet = new ogg_packet();
			this->packet->bytes = data->Length;
			this->packet->packet = new unsigned char[data->Length];
			Marshal::Copy(data, 0, (IntPtr)this->packet->packet, data->Length);
		}

		TheoraPacket(const ogg_packet &packet)
		{
			this->packet = new ogg_packet();
			*this->packet = packet;
			this->packet->packet = new unsigned char[packet.bytes];
			memcpy(this->packet->packet, packet.packet, packet.bytes);
		}

		!TheoraPacket()
		{
			delete[] this->packet->packet;
			delete this->packet;
		}

		~TheoraPacket()
		{
			this->!TheoraPacket();
		}

		int GetCount()
		{
			return this->packet->bytes;
		}

		ogg_packet& To_Ogg_Packet()
		{
			return *(this->packet);
		}

		array<Byte>^ ToArray()
		{
			array<Byte>^ result = gcnew array<Byte>(this->packet->bytes);
			Marshal::Copy((IntPtr)this->packet->packet, result, 0, this->packet->bytes);
			return result;
		}
	};

	public ref class TheoraHeader
	{
	public:
		TheoraHeader()
		{
			Headers = gcnew array<TheoraPacket^>(50);
			Count = 0;
		}

		void Add(TheoraPacket^ headerPacket)
		{
			Headers[Count++] = headerPacket;
		}

		array<TheoraPacket^>^ Headers;
		int Count;
	};

	public ref class Encoder
	{
	private:
		th_enc_ctx *encoder;
		th_info *format;
		th_comment *comment;
		th_ycbcr_buffer *inData;
		TheoraHeader^ header;

	public:

		TheoraHeader^ GetHeader()
		{
			return header;
		}

		Encoder(int width, int height)
		{
			format = new th_info();
			comment = new th_comment();

			th_info_init(format);
			format->frame_width = width;
			format->frame_height = height;
			format->pic_width = width;
			format->pic_height = height;
			format->pic_x = 0;
			format->pic_y = 0;
			format->colorspace = TH_CS_UNSPECIFIED;
			format->pixel_fmt = TH_PF_420;
			format->target_bitrate =  0; // 0 == VBR; b/s
			format->quality = 30; // 0 - 63;
			
			encoder = th_encode_alloc(format);
			if (encoder == 0)
				throw 0; // todo

			header = gcnew TheoraHeader();
			int index = 0;
			ogg_packet headerData;
			th_comment_init(comment);

			int result = 0;
			while((result = th_encode_flushheader(encoder, comment, &headerData)) > 0)
			{
				System::Console::WriteLine("header: " + result + " Length: " + headerData.bytes);
				TheoraPacket^ packet = gcnew TheoraPacket(headerData);
				header->Add(packet);
			}

			int stride = width * 3;
			inData = (th_ycbcr_buffer*)(new th_ycbcr_buffer());
			(*inData)[0].width = (*inData)[1].width = (*inData)[2].width = width;
			(*inData)[0].height = (*inData)[1].height = (*inData)[2].height = height;
			(*inData)[0].stride = (*inData)[1].stride = (*inData)[2].stride = stride;
			
			int bytes = stride * height;
			(*inData)[0].data = new unsigned char[bytes];
			(*inData)[1].data = new unsigned char[bytes];
			(*inData)[2].data = new unsigned char[bytes];
		};

		!Encoder()
		{
			th_encode_free(encoder);
			th_info_clear(format);
			delete format;
			th_comment_clear(comment);
			delete comment;
			delete[] (*inData)[0].data;
			delete[] (*inData)[1].data;
			delete[] (*inData)[2].data;
			delete[] inData;
		}

		~Encoder()
		{
			this->!Encoder();
		}

		int Encode(YCbCrImage^ image, array<TheoraPacket^>^ encodedData)
		{
			(*inData)[0] = *image->Y;
			(*inData)[1] = *image->Cb;
			(*inData)[2] = *image->Cr;

			int result = th_encode_ycbcr_in(encoder, *inData);
			if (result != 0)
			{
				return 0;
			}

			ogg_packet data;
			int totalPackets = 0;
			while (th_encode_packetout(encoder, 0, &data) > 0)
			{
				encodedData[totalPackets++] = gcnew TheoraPacket(data);
			}
			return totalPackets;
		}

	};

	public ref class Decoder
	{
	public:
		Decoder(int width, int height, TheoraHeader^ header)
		{
			format = new th_info();
			th_info_init(format);
			format->frame_width = width;
			format->frame_height = height;
			//format->pic_width = width;
			//format->pic_height = height;
			//format->pic_x = 0;
			//format->pic_y = 0;
			//format->colorspace = TH_CS_UNSPECIFIED;
			format->pixel_fmt = TH_PF_420;
			//format->target_bitrate = 0; // 0 == VBR
			//format->quality = 30; // 0 - 63;

			comment = new th_comment();
			th_comment_init(comment);
			th_setup_info* setup = 0;
			
			int index = 0;
			for (int i = 0; i < header->Count; ++i)
			{
				ogg_packet &headerPacket = header->Headers[i]->To_Ogg_Packet();

				int result = th_decode_headerin(format, comment, &setup, &headerPacket);
				System::Console::WriteLine("Bytes in: " + headerPacket.bytes + " Result: " + result);
			}

			decoder = th_decode_alloc(format, setup);
			if (decoder == 0)
				throw 0; // todo
			
			th_setup_free(setup);
		};

		!Decoder()
		{
			th_decode_free(decoder);
			th_info_clear(format);
			delete format;
			th_comment_clear(comment);
			delete comment;
		}

		~Decoder()
		{
			this->!Decoder();
		}

		void Decode(TheoraPacket^ encodedData, YCbCrImage^ decodedImage)
		{
			ogg_int64_t granpos;
			if (th_decode_packetin(decoder, &encodedData->To_Ogg_Packet(), &granpos) != 0)
				return; // todo

			th_ycbcr_buffer output;
			if (th_decode_ycbcr_out(decoder, output) != 0)
				return; // todo

			*decodedImage->Y = output[0];
			*decodedImage->Cb = output[1];
			*decodedImage->Cr = output[2];
		};

	private:
		th_info *format;
		th_comment *comment;
		th_dec_ctx *decoder;
	};
}
